/**
 * Copyright 2008 Mathias Doenitz, http://lis.to/
 *
 * This file is part of the lis.to java desktop client. The lis.to java desktop client is free software: you can
 * redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * The lis.to java desktop client is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with the lis.to java desktop client.
 * If not, see http://www.gnu.org/licenses/
 */

package listo.utils.reflection;

import listo.utils.MiscUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;

import java.lang.reflect.*;
import java.util.*;

class ReflectorWrapperImpl implements ReflectorWrapper {
    private final Object object;
    private final Class type;
    private final ReflectorImpl reflector;

    protected ReflectorWrapperImpl(Class type, ReflectorImpl reflector) {
        this.object = null;
        this.type = type;
        this.reflector = reflector;
    }

    protected ReflectorWrapperImpl(Object object, ReflectorImpl reflector) {
        this.object = object;
        this.type = object.getClass();
        this.reflector = reflector;
    }

    /**
     * Returns all instantiable classes in the given packages implementing or deriving from the given class or interface.
     *
     * @param packageNames the packages to search through
     * @return the classes
     */
    public List<Class> getNonAbstractImplementingClasses(String... packageNames) {
        List<Class> classes = new LinkedList<Class>();
        for (Class clazz : reflector.getClassesOfPackages(packageNames)) {
            if (canBeCastFrom(clazz) && !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers())) {
                classes.add(clazz);
            }
        }
        return classes;
    }

    /**
     * Checks whether this type can be assigned from the given type.
     *
     * @param subType the type potentially implementing or extending this type
     * @return true if subType is, extends or implements this type
     */
    @SuppressWarnings("unchecked")
    public boolean canBeCastFrom(Class subType) {
        return type.isAssignableFrom(subType);
    }

    /**
     * Checks whether this type can be casted to the given type.
     *
     * @param superType the potential super type
     * @return true if this type is, extends or implements the given type
     */
    @SuppressWarnings("unchecked")
    public boolean canBeCastTo(Class superType) {
        return superType.isAssignableFrom(type);
    }

    /**
     * Gets ALL fields (i.e. private, proctected, public and all inherited).
     *
     * @return the list of all fields this type has
     */
    public List<Field> getAllFields() {
        List<Field> allFields = new ArrayList<Field>();
        for (Class<?> superThing : getAllSuperTypes()) {
            allFields.addAll(Arrays.asList(superThing.getDeclaredFields()));
        }
        return allFields;
    }

    /**
     * Gets the field (public, protected or private, also inherited) with the given name.
     *
     * @param fieldName the name of the field
     * @return the value of the field
     */
    public Field getField(String fieldName) {
        for (Field field : getAllFields()) {
            if (field.getName().equals(fieldName)) return field;
        }
        return null;
    }

    /**
     * Gets the value of the field (public, protected or private, also inherited) with
     * the given name.
     *
     * @param fieldName the name of the field
     * @return the value of the field
     */
    public Object getFieldValue(String fieldName) {
        Field field = getField(fieldName);
        if (field == null) {
            throw new IllegalArgumentException("Illegal field name");
        }
        if (!Modifier.isStatic(field.getModifiers()) && object == null) {
            throw new IllegalStateException("Cannot get instance field value on a class reflector");
        }
        field.setAccessible(true);
        try {
            return field.get(object);
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Sets the value of the field (public, protected or private, also inherited) with
     * the given name.
     *
     * @param fieldName the name of the field
     * @param value     the value of the field
     */
    public void setFieldValue(String fieldName, Object value) {
        Field field = getField(fieldName);
        if (field == null) {
            throw new IllegalArgumentException("Illegal field name");
        }
        if (!Modifier.isStatic(field.getModifiers()) && object == null) {
            throw new IllegalStateException("Cannot set instance field value on a class reflector");
        }
        field.setAccessible(true);
        try {
            field.set(object, value);
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Gets the value of the property (public, protected or private, also inherited) with
     * the given name.
     *
     * @param propertyName the name of the property
     * @return the value of the field
     */
    @SuppressWarnings("unchecked")
    public Object getPropertyValue(String propertyName) {
        try {
            Method getter = type.getMethod("get" + propertyName);
            if (!Modifier.isStatic(getter.getModifiers()) && object == null) {
                throw new IllegalStateException("Cannot get instance property value on a class reflector");
            }
            return getter.invoke(object);
        }
        catch (NoSuchMethodException e) {
            throw new IllegalArgumentException("Illegal method name");
        }
        catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Sets the value of the property (public, protected or private, also inherited) with
     * the given name.
     *
     * @param propertyName the name of the field
     * @param value        the value of the field
     */
    public void setPropertyValue(String propertyName, Object value) {
        try {
            Method[] methods = type.getMethods();
            String setterName = "set" + propertyName;
            Method setter = null;
            for (Method method : methods) {
                if (setterName.equals(method.getName())) {
                    setter = method;
                    break;
                }
            }
            if (setter == null) throw new NoSuchMethodException();
            if (!Modifier.isStatic(setter.getModifiers()) && object == null) {
                throw new IllegalStateException("Cannot get instance property value on a class reflector");
            }
            setter.invoke(object, value);
        }
        catch (NoSuchMethodException e) {
            throw new IllegalArgumentException("Illegal method name");
        }
        catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Returns the set of all super types (classes and interfaces) of this type.
     * This type itself is also included.
     *
     * @return the set
     */
    public Set<Class> getAllSuperTypes() {
        Set<Class> result = new HashSet<Class>();
        getAllSuperTypes(type, result);
        return result;
    }

    // private helper
    private static void getAllSuperTypes(Class type, Set<Class> result) {
        if (type != null) {
            result.add(type);
            getAllSuperTypes(type.getSuperclass(), result);
            for (Class interfaceClass : type.getInterfaces()) {
                getAllSuperTypes(interfaceClass, result);
            }
        }
    }

    /**
     * Find the best matching handler class for the object/class based on the given name pattern.
     *
     * @param namePattern the class name pattern to base the search on
     * @return the class
     */
    public Class getHandlerClass(String namePattern) {
        return reflector.getHandlerClass(type, namePattern);
    }

    /**
     * Perfoms a depth-first tree walk of this objects object graph starting with this object.
     * Circular references are ignored (i.e. only processed on the first occurrence).
     *
     * @param action the action to perform on each object
     * @param types  an optional list of types to limit the action to
     */
    public void traverseObjectTree(ObjectTreeWalkAction action, Class... types) {
        if (object == null) return;
        Set<Object> visited = new HashSet<Object>();
        traverseObjectTree(reflector, object, action, types.length > 0 ? types : null, visited);
    }

    // private helper
    @SuppressWarnings("unchecked")
    private static boolean traverseObjectTree(Reflector reflector, Object object, ObjectTreeWalkAction action,
                                              Class[] types, Set<Object> visited) {
        Class type = object.getClass();
        boolean process = (types == null);
        if (!process) {
            for (Class superType : types) {
                if (superType.isAssignableFrom(type)) {
                    process = true;
                    break;
                }
            }
        }
        if (process && !action.process(object)) return false;
        visited.add(object);

        if (type.isArray()) {
            for (int i = 0; i < Array.getLength(object); i++) {
                Object item = Array.get(object, i);
                if (item == null || visited.contains(item)) continue;
                if (!traverseObjectTree(reflector, item, action, types, visited)) return false;
            }
            return true;
        }

        List<Field> fields = reflector.f(object).getAllFields();
        for (Field field : fields) {
            if (Modifier.isStatic(field.getModifiers())) continue; // we don't care about static fields

            Object fieldValue;
            try {
                field.setAccessible(true);
                fieldValue = field.get(object);
            }
            catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }

            if (fieldValue == null || visited.contains(fieldValue)) continue;

            if (!traverseObjectTree(reflector, fieldValue, action, types, visited)) return false;
        }
        return true;
    }

    /**
     * Fills all fields of the object with the from String values from the given properties.
     * Caution: "null" strings are loaded as null values!
     *
     * @param properties the properties
     */
    public void loadFromProperties(Properties properties) {
        List<Field> fields = getAllFields();
        for (Field field : fields) {
            String fieldName = field.getName();
            Class fieldType = field.getType();

            String value = properties.getProperty(fieldName);
            if (value == null) continue;

            if ("null".equals(value)){
                setFieldValue(fieldName, null);
                continue;
            }

            if (fieldType.isArray()) {
                String[] values = StringUtils.split(value, ',');
                Class componentType = fieldType.getComponentType();
                Object array = Array.newInstance(componentType, values.length);

                for (int i = 0; i < values.length; i++) {
                    Array.set(array, i, MiscUtils.convertFromString(values[i].trim(), componentType));
                }
                setFieldValue(fieldName, array);
            } else {
                setFieldValue(fieldName, MiscUtils.convertFromString(value, fieldType));
            }
        }
    }

    /**
     * Creates a Properties object filled with all fieldnames (keys) and their toString values (values).
     * Caution: null values are saved as "null" strings!
     *
     * @return the generated Properties
     */
    public Properties saveToProperties() {
        return saveToProperties(true);
    }

    /**
     * Creates a Properties object filled with all fieldnames (keys) and their toString values (values).
     * Caution: null values are saved as "null" strings!
     *
     * @param includeFinalFields set to false to ignore final fields
     * @return the generated Properties
     */
    public Properties saveToProperties(boolean includeFinalFields) {
        Properties properties = new Properties();
        List<Field> fields = getAllFields();
        for (Field field : fields) {
            if (!includeFinalFields && Modifier.isFinal(field.getModifiers())) continue;

            String fieldName = field.getName();
            Object value = getFieldValue(fieldName);
            if (value == null) value = "null";
            if (value instanceof int[]) value = ArrayUtils.toObject((int[]) value);
            if (value instanceof float[]) value = ArrayUtils.toObject((float[]) value);
            if (value instanceof double[]) value = ArrayUtils.toObject((double[]) value);
            if (value instanceof char[]) value = ArrayUtils.toObject((char[]) value);
            if (value instanceof boolean[]) value = ArrayUtils.toObject((boolean[]) value);
            if (value instanceof long[]) value = ArrayUtils.toObject((long[]) value);
            properties.put(fieldName,
                    value instanceof Object[] ? StringUtils.join((Object[]) value, ',') : value.toString());
        }
        return properties;
    }

}
